package com.freedom.common.db.handler;

import lombok.RequiredArgsConstructor;
import org.apache.ibatis.type.*;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.sql.*;
import java.time.*;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;


@RequiredArgsConstructor
@MappedTypes({ArrayList.class})
@MappedJdbcTypes({JdbcType.ARRAY})
public class ArrayListHandler extends BaseTypeHandler<Object> {

    private static final ConcurrentHashMap<Class<?>, String> STANDARD_MAPPING;

    static {
        STANDARD_MAPPING = new ConcurrentHashMap<>();
        STANDARD_MAPPING.put(BigDecimal.class, JdbcType.NUMERIC.name());
        STANDARD_MAPPING.put(BigInteger.class, JdbcType.BIGINT.name());
        STANDARD_MAPPING.put(boolean.class, JdbcType.BOOLEAN.name());
        STANDARD_MAPPING.put(Boolean.class, JdbcType.BOOLEAN.name());
        STANDARD_MAPPING.put(byte[].class, JdbcType.VARBINARY.name());
        STANDARD_MAPPING.put(byte.class, JdbcType.TINYINT.name());
        STANDARD_MAPPING.put(Byte.class, JdbcType.TINYINT.name());
        STANDARD_MAPPING.put(Calendar.class, JdbcType.TIMESTAMP.name());
        STANDARD_MAPPING.put(Date.class, JdbcType.DATE.name());
        STANDARD_MAPPING.put(java.util.Date.class, JdbcType.TIMESTAMP.name());
        STANDARD_MAPPING.put(double.class, JdbcType.DOUBLE.name());
        STANDARD_MAPPING.put(Double.class, JdbcType.DOUBLE.name());
        STANDARD_MAPPING.put(float.class, JdbcType.REAL.name());
        STANDARD_MAPPING.put(Float.class, JdbcType.REAL.name());
        STANDARD_MAPPING.put(int.class, JdbcType.INTEGER.name());
        STANDARD_MAPPING.put(Integer.class, JdbcType.INTEGER.name());
        STANDARD_MAPPING.put(LocalDate.class, JdbcType.DATE.name());
        STANDARD_MAPPING.put(LocalDateTime.class, JdbcType.TIMESTAMP.name());
        STANDARD_MAPPING.put(LocalTime.class, JdbcType.TIME.name());
        STANDARD_MAPPING.put(long.class, JdbcType.BIGINT.name());
        STANDARD_MAPPING.put(Long.class, JdbcType.BIGINT.name());
        STANDARD_MAPPING.put(OffsetDateTime.class, JdbcType.TIMESTAMP_WITH_TIMEZONE.name());
        STANDARD_MAPPING.put(OffsetTime.class, JdbcType.TIME_WITH_TIMEZONE.name());
        STANDARD_MAPPING.put(Short.class, JdbcType.SMALLINT.name());
        STANDARD_MAPPING.put(String.class, JdbcType.VARCHAR.name());
        STANDARD_MAPPING.put(Time.class, JdbcType.TIME.name());
        STANDARD_MAPPING.put(Timestamp.class, JdbcType.TIMESTAMP.name());
        STANDARD_MAPPING.put(URL.class, JdbcType.DATALINK.name());
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
            throws SQLException {
        if (parameter instanceof ArrayList) {
            ArrayList<?> list = (ArrayList<?>) parameter;
            if (list.isEmpty()) {
                ps.setNull(i, Types.ARRAY);
            } else {
                Class<?> componentType = list.get(0).getClass();
                String arrayTypeName = resolveTypeName(componentType);
                Array array = ps.getConnection().createArrayOf(arrayTypeName, ((ArrayList<?>) parameter).toArray());
                ps.setArray(i, array);
                array.free();
            }
        } else {
            throw new TypeException("ArrayType Handler requires SQL array or java list parameter and does not support type " + parameter.getClass());
        }
    }

    protected String resolveTypeName(Class<?> type) {
        return STANDARD_MAPPING.getOrDefault(type, JdbcType.JAVA_OBJECT.name());
    }

    @Override
    public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return extractArray(rs.getArray(columnName));
    }

    @Override
    public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return extractArray(rs.getArray(columnIndex));
    }

    @Override
    public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return extractArray(cs.getArray(columnIndex));
    }

    protected Object extractArray(Array array) throws SQLException {
        if (array == null) {
            return null;
        }
        List<Object> arrList = new ArrayList<>();
        ResultSet resultSet = array.getResultSet();
        while (resultSet.next()) {
            switch (array.getBaseType()) {
                case Types.INTEGER:
                    arrList.add(resultSet.getInt("VALUE"));
                    break;
                case Types.NUMERIC:
                    arrList.add(resultSet.getBigDecimal("VALUE"));
                    break;
                default:
                    arrList.add(resultSet.getString("VALUE"));
                    break;
            }
        }
        array.free();
        return arrList;
    }
}
